
/*
 * Janino - An embedded Java[TM] compiler
 *
 * Copyright (c) 2017 Arno Unkrig. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
 * following conditions are met:
 *
 *    1. Redistributions of source code must retain the above copyright notice, this list of conditions and the
 *       following disclaimer.
 *    2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
 *       following disclaimer in the documentation and/or other materials provided with the distribution.
 *    3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
 *       products derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package org.codehaus.janino.tests;

import java.io.FileReader;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.lang.reflect.InvocationTargetException;
import java.nio.charset.UnsupportedCharsetException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.codehaus.commons.compiler.CompileException;
import org.codehaus.commons.compiler.IClassBodyEvaluator;
import org.codehaus.commons.compiler.util.reflect.ByteArrayClassLoader;
import org.codehaus.commons.compiler.util.resource.MapResourceCreator;
import org.codehaus.commons.compiler.util.resource.MapResourceFinder;
import org.codehaus.commons.compiler.util.resource.Resource;
import org.codehaus.commons.nullanalysis.Nullable;
import org.codehaus.janino.ClassLoaderIClassLoader;
import org.codehaus.janino.ExpressionEvaluator;
import org.codehaus.janino.IClassLoader;
import org.codehaus.janino.Java;
import org.codehaus.janino.Java.AbstractCompilationUnit;
import org.codehaus.janino.Java.BlockStatement;
import org.codehaus.janino.Java.CompilationUnit;
import org.codehaus.janino.Java.IntegerLiteral;
import org.codehaus.janino.Java.Rvalue;
import org.codehaus.janino.Parser;
import org.codehaus.janino.Scanner;
import org.codehaus.janino.ScriptEvaluator;
import org.codehaus.janino.SimpleCompiler;
import org.codehaus.janino.UnitCompiler;
import org.codehaus.janino.UnitCompiler.ClassFileConsumer;
import org.codehaus.janino.Unparser;
import org.codehaus.janino.util.ClassFile;
import org.codehaus.janino.util.ClassFile.AttributeInfo;
import org.codehaus.janino.util.ClassFile.CodeAttribute;
import org.codehaus.janino.util.ClassFile.MethodInfo;
import org.codehaus.janino.util.DeepCopier;
import org.junit.Assert;
import org.junit.Ignore;
import org.junit.Test;

/**
 * Tests that reproduce <a href="https://github.com/janino-compiler/janino/issues">the issues reported on GITHUB</a>.
 */
public
class GithubIssuesTest {

    /**
     * @see <a href="https://github.com/janino-compiler/janino/issues/19">GITHUB issue #19: Get bytecode after
     *      compile</a>
     */
    @Test public void
    testCompileToBytecode() throws CompileException {

        // Set up an ExpressionCompiler and cook the expression.
        ExpressionEvaluator ee = new ExpressionEvaluator();
        ee.setExpressionType(int.class);

        ee.cook("7");

        // Retrieve the generated bytecode from the ExpressionCompiler. The result is a map from class name
        // to the class's bytecode.
        Map<String, byte[]> result = ee.getBytecodes();
        Assert.assertNotNull(result);

        // verify that exactly _one_ class was generated.
        Assert.assertEquals(1, result.size());

        // Verify the class's name.
        byte[] ba = result.get(IClassBodyEvaluator.DEFAULT_CLASS_NAME);
        Assert.assertNotNull(ba);

        // Verify that the generated bytecode looks "reasonable", i.e. starts with the charcteristic
        // "magic bytes" and has an approximate size.
        Assert.assertArrayEquals(
            new byte[] { (byte) 0xca, (byte) 0xfe, (byte) 0xba, (byte) 0xbe },
            Arrays.copyOf(ba, 4)
        );
        Assert.assertTrue(Integer.toString(ba.length), ba.length > 150);
        Assert.assertTrue(Integer.toString(ba.length), ba.length < 300);
    }

    @SuppressWarnings({ "unchecked", "rawtypes" }) @Test public void
    testExpressionCompilerMethods() throws CompileException, IOException, InvocationTargetException {

        Class<Comparable> interfaceToImplement = Comparable.class;
        String[]          parameterNames       = { "o" };
        Class<?>[]        parameterTypes       = { Object.class };
        String            e                    = "7";
        final Object[]    arguments            = { "" };

        CASES: for (int i = 0;; i++) {

            ExpressionEvaluator ee = new ExpressionEvaluator();

            // All these invocations are expected to throw an IllegalStateException.
            switch (i) {

            case 0: Assert.assertEquals(7, ee.createFastEvaluator(new StringReader(e), interfaceToImplement, parameterNames).compareTo("foo"));                    break;
            case 1: Assert.assertEquals(7, ee.createFastEvaluator(new Scanner(null, new StringReader(e)), interfaceToImplement, parameterNames).compareTo("foo")); break;
            case 2: Assert.assertEquals(7, ee.createFastEvaluator(e, interfaceToImplement, parameterNames).compareTo("foo"));                                      break;
//            case 3: ee.setExpressionType(int.class); ee.cook(e); Assert.assertEquals(7, ee.evaluate(arguments));                                                   break;
//            case 4: ee.setExpressionType(int.class); ee.cook(e); Assert.assertEquals(7, ee.evaluate(0, arguments));                                                break;
//            case 5: ee.setExpressionType(int.class); ee.cook(e); Assert.assertNotNull(ee.getClazz());                                                              break;
//            case 6: ee.setExpressionType(int.class); ee.cook(e); Assert.assertNotNull(ee.getMethod());                                                             break;
//            case 7: ee.setExpressionType(int.class); ee.cook(e); Assert.assertNotNull(ee.getMethod(0));                                                            break;

            case /*8*/3: break CASES;

            default: continue;
            }
        }

        {
            ExpressionEvaluator ee = new ExpressionEvaluator();
            ee.setExpressionType(int.class);
            ee.setParameters(parameterNames, parameterTypes);
            ee.cook(e);

            Assert.assertEquals(7, ee.evaluate(arguments));
            Assert.assertEquals(7, ee.evaluate(0, arguments));
            Assert.assertNotNull(ee.getClazz());
            Assert.assertNotNull(ee.getMethod());
            Assert.assertNotNull(ee.getMethod(0));
        }
    }

    @Test public void
    testIssue91() throws Exception {

        // Parse the framework code.
        CompilationUnit cu = (CompilationUnit) new Parser(new Scanner(
            "user expression", // This will appear in stack traces as "file name".
            new StringReader(
                ""
                + "package com.acme.framework;\n"
                + "\n"
                + "public\n"
                + "class Framework {\n"
                + "\n"
                + "    public static void\n"
                + "    frameworkMethod() {\n"
                + "        System.out.println(\"frameworkMethod()\");\n"
                + "    }\n"
                + "\n"
                + "    public static void\n"
                + "    userMethod() {\n"
                + "        System.out.println(77);\n" // <= "77" will be replaced with the user expression!
                + "    }\n"
                + "}\n"
            )
        )).parseAbstractCompilationUnit();

        // Parse the "user expression".
        final Rvalue userExpression;
        {
            Scanner s = new Scanner(
                null,             // fileName
                new StringReader( // Line numbers will appear in stack traces.
                    ""
                    + "\n"
                    + "\n"
                    + "java.nio.charset.Charset.forName(\"kkk\")\n" // <= Causes an UnsupportedCharsetException
                )
            );
            userExpression = new Parser(s).parseExpression();
        }

        // Merge the framework code with the user expression.
        cu = new DeepCopier() {

            @Override public Rvalue
            copyIntegerLiteral(IntegerLiteral subject) throws CompileException {
                if ("77".equals(subject.value)) return userExpression;
                return super.copyIntegerLiteral(subject);
            }
        }.copyCompilationUnit(cu);

        // Compile the code into a ClassLoader.
        SimpleCompiler sc = new SimpleCompiler();
        sc.setDebuggingInformation(true, true, true);
        sc.cook(cu);
        ClassLoader cl = sc.getClassLoader();

        // Find the generated class by name.
        Class<?> c = cl.loadClass("com.acme.framework.Framework");

        // Invoke the class's methods.
        c.getMethod("frameworkMethod").invoke(null);
        try {
            c.getMethod("userMethod").invoke(null);
            Assert.fail("InvocationTargetException expected");
        } catch (InvocationTargetException ite) {
            Throwable te = ite.getTargetException();
            Assert.assertEquals(UnsupportedCharsetException.class, te.getClass());
            StackTraceElement top = te.getStackTrace()[1];
            Assert.assertEquals("userMethod", top.getMethodName());
            Assert.assertEquals("user expression", top.getFileName());
            Assert.assertEquals(3, top.getLineNumber());
        }
    }

    // This is currently failing
    // https://github.com/codehaus/janino/issues/4
    @Ignore
    @Test public void
    testReferenceQualifiedSuper() throws Exception {
        GithubIssuesTest.doCompile(true, true, false, GithubIssuesTest.RESOURCE_DIR + "/a/Test.java");
    }
    private static final String RESOURCE_DIR = "../commons-compiler-tests/src/test/resources";

    @Test public void
    testIssue102() throws Exception {
        CompileUnit unit1 = new CompileUnit("demo.pkg1", "A$$1", (
            ""
            + "package demo.pkg1;\n"
            + "import demo.pkg2.*;\n"
            + "public class A$$1 {\n"
            + "    public static String main() { return B$1.hello(); }\n"
            + "    public static String hello() { return \"HELLO\"; }\n"
            + "}"
        ));
        CompileUnit unit2 = new CompileUnit("demo.pkg2", "B$1", (
            ""
            + "package demo.pkg2;\n"
            + "import demo.pkg1.*;\n"
            + "public class B$1 {\n"
            + "    public static String hello() { return \"HELLO\" /*A$$1.hello()*/; }\n"
            + "}"
        ));

        ClassLoader
        classLoader = this.compile(Thread.currentThread().getContextClassLoader(), unit1, unit2);

        Assert.assertEquals(
            "HELLO",
            classLoader.loadClass("demo.pkg1.A$$1").getMethod("main").invoke(null)
        );
    }

    @Test public void
    testIssue113() throws Exception {
        CompileUnit unit1 = new CompileUnit("demo.pkg3", "A$$1", (
            ""
            + "package demo.pkg3;\n"
            + "public class A$$1 {\n"
            + "    public static String main() {\n"
            + "        StringBuilder sb = new StringBuilder();\n"
            + "        short b = 1;\n"
            + "        for (int i = 0; i < 4; i++) {\n"
            + "            ;\n"
            + "            switch (i) {\n"
            + "                case 0:\n"
            + "                    sb.append(\"A\");\n"
            + "                    break;\n"
            + "                case 1:\n"
            + "                    sb.append(\"B\");\n"
            + "                    break;\n"
            + "                case 2:\n"
            + "                    sb.append(\"C\");\n"
            + "                    break;\n"
            + "                case 3:\n"
            + "                    sb.append(\"D\");\n"
            + "                    break;\n"
            + "            }\n"
            + GithubIssuesTest.injectDummyLargeCodeExceedingShort()
            + "        }\n"
            + "        return sb.toString();\n"
            + "    }\n"
            + "}\n"
        ));

        ClassLoader classLoader = this.compile(Thread.currentThread().getContextClassLoader(), unit1);

        Assert.assertEquals("ABCD", classLoader.loadClass("demo.pkg3.A$$1").getMethod("main").invoke(null));
    }

    private static String
    injectDummyLargeCodeExceedingShort() {
        StringBuilder sb = new StringBuilder();
        sb.append("int a = -1;\n");
        for (int i = 0; i < Short.MAX_VALUE / 3; i++) {
            sb.append("a = " + i + ";\n");
        }
        return sb.toString();
    }

    @Test public void
    testIssue135() throws Exception {

        Java.AbstractCompilationUnit cu = new Parser(new Scanner("Test", new StringReader(
            ""
            + "public class Test {"
            + "   int blup;"
            + "}"
        ))).parseAbstractCompilationUnit();

        cu = new DeepCopier().copyAbstractCompilationUnit(cu);

        SimpleCompiler sc    = new SimpleCompiler();
        boolean        works = false;
        if (works) {
            StringWriter sw = new StringWriter();
            Unparser.unparse(cu, sw);
            sc.cook(sw.toString());
        } else {
            sc.cook(cu);
        }
        Assert.assertNotNull(sc.getClassLoader().loadClass("Test").getDeclaredConstructor().newInstance());
    }

    @Test public void
    testIssue166() throws Exception {
        String s = (
            ""
            + "package foo;\n"
            + "public\n"
            + "class Main {\n"
            + "    public static void\n"
            + "    main() {\n"
            + "       System.out.println(\"HELLO WORLD!\");\n"
            + "    }\n"
            + "}\n"
        );

        AbstractCompilationUnit
        acu = new Parser(new Scanner("fileName", new StringReader(s))).parseAbstractCompilationUnit();

        IClassLoader icl = new ClassLoaderIClassLoader();

        final int[] bcs = { -1 };
        new UnitCompiler(acu, icl).compileUnit(false, false, false, new ClassFileConsumer() {

            @Override public void
            consume(ClassFile cf) {
                if ("foo.Main".equals(cf.getThisClassName())) {
                    for (MethodInfo mi : cf.methodInfos) {
                        if ("main".equals(mi.getName())) {
                            for (AttributeInfo ai : mi.getAttributes()) {
                                if (ai instanceof CodeAttribute) {
                                    CodeAttribute ca = (CodeAttribute) ai;

                                    Assert.assertEquals(-1, bcs[0]);
                                    bcs[0] = ca.code.length;
                                }
                            }
                        }
                    }
                }
            }
        });

        Assert.assertEquals(9, bcs[0]);
    }

    @Test public void
    testIssue167() throws Exception {
        new ScriptEvaluator().cook("double[] /* <= any primitive array */ arr1;");
        new ScriptEvaluator().cook("Object x2 = ((double[] /* <= same primitive array type */) null).toString();");
        // This led to "Assignment conversion not possible from type "java.lang.String" to type "java.lang.Object"".
    }

    @Test public void
    testIssue177() throws Exception {
        SimpleCompiler sc = new SimpleCompiler();
        sc.cook(
            ""
            + "public class test2 {\n"
            + "  public final static double x = 0;\n"
            + "  public static void main(String[] args){\n"
            + "    System.out.println(\"Hello World!\");\n"
            + "  }\n"
            + "}\n"
        );
    }

    @Test public void
    testIssue178() throws Exception {
        SimpleCompiler sc = new SimpleCompiler();
        sc.cook(
            ""
            + "public\n"
            + "class test3 {\n"
            + "\n"
            + "    public static void\n"
            + "    main(String[] args){\n"
            + "        double a;\n"
            + "        for (int i = 0; i < 3; ++i) {\n"
            + "            a = i;\n"
            + "            System.out.println(a);\n"
            + "        }\n"
            + "    }\n"
            + "}\n"
        );
    }

    public ClassLoader
    compile(ClassLoader parentClassLoader, CompileUnit... compileUnits) {

        MapResourceFinder sourceFinder = new MapResourceFinder();
        for (CompileUnit unit : compileUnits) {
            String stubFileName = unit.pkg.replace(".", "/") + "/" + unit.mainClassName + ".java";
            sourceFinder.addResource(stubFileName, unit.getCode());
        }

        // Storage for generated bytecode
        final Map<String, byte[]> classes = new HashMap<>();

        // Set up the compiler.
        org.codehaus.janino.Compiler compiler = new org.codehaus.janino.Compiler();
        compiler.setSourceFinder(sourceFinder);
        compiler.setIClassLoader(new ClassLoaderIClassLoader(parentClassLoader));
        compiler.setClassFileCreator(new MapResourceCreator(classes));
        compiler.setClassFileFinder(new MapResourceFinder(classes));

        // Compile all sources
        try {
            compiler.compile(sourceFinder.resources().toArray(new Resource[0]));
        } catch (CompileException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

        // Set up a class loader that finds and defined the generated classes.
        return new ByteArrayClassLoader(classes, parentClassLoader);
    }

    interface Supplier<T> { T get(); }

    public
    class CompileUnit {
        private final String               pkg;
        private final String               mainClassName;
        @Nullable private String           code;
        @Nullable private Supplier<String> genCodeFunc;

        public
        CompileUnit(String pkg, String mainClassName, String code) {
            this.pkg           = pkg;
            this.mainClassName = mainClassName;
            this.code          = code;
        }

        public
        CompileUnit(String pkg, String mainClassName, Supplier<String> genCodeFunc) {
            this.pkg           = pkg;
            this.mainClassName = mainClassName;
            this.genCodeFunc   = genCodeFunc;
        }

        public String
        getCode() {
            if (this.code != null) return this.code;

//            Preconditions.checkNotNull(this.genCodeFunc);
            assert this.genCodeFunc != null;
            this.code = this.genCodeFunc.get();
            return this.code;
        }

        @Override public String
        toString() { return "CompileUnit{ pkg=" + this.pkg + ", mainClassName='" + this.mainClassName + " }"; }
    }

    public static List<ClassFile>
    doCompile(
        boolean   debugSource,
        boolean   debugLines,
        boolean   debugVars,
        String... fileNames
    ) throws Exception {

        // Parse each compilation unit.
        final List<AbstractCompilationUnit> acus = new LinkedList<>();
        final IClassLoader                  cl   = new ClassLoaderIClassLoader(GithubIssuesTest.class.getClassLoader());
        List<ClassFile>                     cfs  = new LinkedList<>();
        for (String fileName : fileNames) {

            FileReader r = new FileReader(fileName);
            try {
                Java.AbstractCompilationUnit acu = new Parser(new Scanner(fileName, r)).parseAbstractCompilationUnit();
                acus.add(acu);

                // Compile them.
                new UnitCompiler(acu, cl).compileUnit(debugSource, debugLines, debugVars, cfs);
            } finally {
                try { r.close(); } catch (Exception e) {}
            }
        }
        return cfs;
    }

    @Test public void
    testIssue196() throws Exception {

        GithubIssuesTest.assertParseUnparseEquals("BiConsumer<String, List<String>> a;");
        GithubIssuesTest.assertParseUnparseEquals("BiConsumer<List<String>, String> a;");
    }

    private static void
    assertParseUnparseEquals(String sourceCode) throws CompileException, IOException {

        BlockStatement bs = new Parser(new org.codehaus.janino.Scanner("filename", new StringReader(
            sourceCode
        ))).parseBlockStatement();

        StringWriter sw       = new StringWriter();
        Unparser     unparser = new Unparser(sw);
        unparser.unparseBlockStatement(bs);
        unparser.close();

        Assert.assertEquals(sourceCode, sw.toString());
    }
}
